use std::{collections::HashSet, sync::Arc};

use lazy_static::lazy_static;
use ruma::{
    api::client::error::ErrorKind, canonical_json::to_canonical_value, events::TimelineEventType,
    state_res::RoomVersion, uint, CanonicalJsonObject, CanonicalJsonValue, EventId, OwnedEventId,
    OwnedServerName, RoomId, RoomVersionId, UserId,
};
use serde_json::value::to_raw_value;
use tokio::sync::OwnedMutexGuard;

use crate::{
    core::pdu::{builder::PduBuilder, EventHash, PduEvent},
    server::{global, sending, Config, CONFIG},
    utils::{self, time::now_millis},
    Error, Result,
};

use tracing::error;

use super::state_cache;
pub struct Service {
    services: Services,
}

lazy_static! {
    pub static ref SERVICE: Arc<Service> = Arc::new(Service::build());
}

struct Services {
    config: Config,
    global: Arc<global::Service>,
    state_cache: Arc<state_cache::Service>,
    sending: Arc<sending::Service>,
}

impl Service {
    pub fn build() -> Self {
        Self {
            services: Services {
                config: CONFIG.clone(),
                global: global::SERVICE.clone(),
                state_cache: state_cache::SERVICE.clone(),
                sending: sending::SERVICE.clone(),
            },
        }
    }

    pub fn get_pdu(&self, event_id: &EventId) -> Result<Option<PduEvent>> {
        Ok(None)
    }

    pub async fn append_pdu(
        &self,
        pdu: &PduEvent,
        pdu_json: CanonicalJsonObject,
        new_room_leaves: Vec<OwnedEventId>,
        state_lock: &OwnedMutexGuard<()>, // Take mutex guard to make sure users get the room state mutex
    ) -> Result<Arc<EventId>> {
        Ok(pdu.event_id.clone())
    }
    pub fn create_hash_and_sign_event(
        &self,
        pdu_builder: PduBuilder,
        sender: &UserId,
        room_id: &RoomId,
        _mutex_lock: &OwnedMutexGuard<()>, // Take mutex guard to make sure users get the room state mutex
    ) -> Result<(PduEvent, CanonicalJsonObject)> {
        let PduBuilder {
            event_type,
            content,
            unsigned,
            state_key,
            redacts,
            timestamp,
        } = pdu_builder;

        // let prev_events: Vec<_> = self
        //     .services
        //     .state
        //     .get_forward_extremities(room_id)?
        //     .into_iter()
        //     .take(20)
        //     .collect();

        // If there was no create event yet, assume we are creating a room
        // let room_version_id = self.services.state.get_room_version(room_id).or_else(|_| {
        //     if event_type == TimelineEventType::RoomCreate {
        //         let content = serde_json::from_str::<RoomCreateEventContent>(content.get())
        //             .expect("Invalid content in RoomCreate pdu.");
        //         Ok(content.room_version)
        //     } else {
        //         Err(Error::InconsistentRoomState(
        //             "non-create event for room of unknown version",
        //             room_id.to_owned(),
        //         ))
        //     }
        // })?;
        let room_version_id = ruma::RoomVersionId::V10;

        let room_version = RoomVersion::new(&room_version_id).expect("room version is supported");

        // let auth_events = self.services.state.get_auth_events(
        //     room_id,
        //     &event_type,
        //     sender,
        //     state_key.as_deref(),
        //     &content,
        // )?;

        // Our depth is the maximum depth of prev_events + 1
        // let depth = prev_events
        //     .iter()
        //     .filter_map(|event_id| Some(self.get_pdu(event_id).ok()??.depth))
        //     .max()
        //     .unwrap_or_else(|| uint!(0))
        //     .saturating_add(uint!(1));

        let mut unsigned = unsigned.unwrap_or_default();

        // if let Some(state_key) = &state_key {
        //     if let Some(prev_pdu) = self.services.state_accessor.room_state_get(
        //         room_id,
        //         &event_type.to_string().into(),
        //         state_key,
        //     )? {
        //         unsigned.insert(
        //             "prev_content".to_owned(),
        //             serde_json::from_str(prev_pdu.content.get()).expect("string is valid json"),
        //         );
        //         unsigned.insert(
        //             "prev_sender".to_owned(),
        //             serde_json::to_value(&prev_pdu.sender).expect("UserId::to_value always works"),
        //         );
        //         unsigned.insert(
        //             "replaces_state".to_owned(),
        //             serde_json::to_value(&prev_pdu.event_id).expect("EventId is valid json"),
        //         );
        //     }
        // }

        let mut pdu = PduEvent {
            event_id: ruma::event_id!("$thiswillbefilledinlater").into(),
            room_id: room_id.to_owned(),
            sender: sender.to_owned(),
            origin: None,
            origin_server_ts: timestamp.map_or_else(
                || now_millis().try_into().expect("u64 fits into UInt"),
                |ts| ts.get(),
            ),
            kind: event_type,
            content,
            state_key,
            prev_events: Vec::new(),
            depth: uint!(0),
            auth_events: Vec::new(),
            // auth_events
            //     .values()
            //     .map(|pdu| pdu.event_id.clone())
            //     .collect(),
            redacts,
            unsigned: if unsigned.is_empty() {
                None
            } else {
                Some(to_raw_value(&unsigned).expect("to_raw_value always works"))
            },
            hashes: EventHash {
                sha256: "aaa".to_owned(),
            },
            signatures: None,
        };

        // let auth_check = state_res::auth_check(
        //     &room_version,
        //     &pdu,
        //     None::<PduEvent>, // TODO: third_party_invite
        //     |k, s| auth_events.get(&(k.clone(), s.to_owned())),
        // )
        // .map_err(|e| {
        //     error!("Auth check failed: {:?}", e);
        //     Error::BadRequest(ErrorKind::forbidden(), "Auth check failed.")
        // })?;

        // if !auth_check {
        //     return Err(Error::BadRequest(
        //         ErrorKind::forbidden(),
        //         "Event is not authorized.",
        //     ));
        // }

        // Hash and sign
        let mut pdu_json = utils::json::to_canonical_object(&pdu).map_err(|e| {
            error!("Failed to convert PDU to canonical JSON: {e}");
            Error::bad_database("Failed to convert PDU to canonical JSON.")
        })?;

        // room v3 and above removed the "event_id" field from remote PDU format
        match room_version_id {
            RoomVersionId::V1 | RoomVersionId::V2 => {}
            _ => {
                pdu_json.remove("event_id");
            }
        };

        // Add origin because synapse likes that (and it's required in the spec)
        pdu_json.insert(
            "origin".to_owned(),
            to_canonical_value(self.services.config.server_name.clone())
                .expect("server name is a valid CanonicalJsonValue"),
        );

        match ruma::signatures::hash_and_sign_event(
            self.services.config.server_name.as_str(),
            self.services.global.keypair(),
            &mut pdu_json,
            &room_version_id,
        ) {
            Ok(()) => {}
            Err(e) => {
                return match e {
                    ruma::signatures::Error::PduSize => Err(Error::BadRequest(
                        ErrorKind::TooLarge,
                        "Message is too long",
                    )),
                    _ => Err(Error::BadRequest(
                        ErrorKind::Unknown,
                        "Signing event failed",
                    )),
                }
            }
        }

        // Generate event id
        pdu.event_id = EventId::parse_arc(format!(
            "${}",
            ruma::signatures::reference_hash(&pdu_json, &room_version_id)
                .expect("ruma can calculate reference hashes")
        ))
        .expect("ruma's reference hashes are valid event ids");

        pdu_json.insert(
            "event_id".to_owned(),
            CanonicalJsonValue::String(pdu.event_id.as_str().to_owned()),
        );

        // Generate short event id
        // let _shorteventid = self
        //     .services
        //     .short
        //     .get_or_create_shorteventid(&pdu.event_id)?;

        Ok((pdu, pdu_json))
    }

    /// Creates a new persisted data unit and adds it to a room. This function
    /// takes a roomid_mutex_state, meaning that only this function is able to
    /// mutate the room state.
    // #[tracing::instrument(skip(self))]
    pub async fn build_and_append_pdu(
        &self,
        pdu_builder: PduBuilder,
        sender: &UserId,
        room_id: &RoomId,
        state_lock: &OwnedMutexGuard<()>, // Take mutex guard to make sure users get the room state mutex
    ) -> Result<Arc<EventId>> {
        let (pdu, pdu_json) =
            self.create_hash_and_sign_event(pdu_builder, sender, room_id, state_lock)?;

        // If redaction event is not authorized, do not append it to the timeline
        // if pdu.kind == TimelineEventType::RoomRedaction {
        //     use RoomVersionId::*;
        //     match self.services.state.get_room_version(&pdu.room_id)? {
        //         V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 => {
        //             if let Some(redact_id) = &pdu.redacts {
        //                 if !self.services.state_accessor.user_can_redact(
        //                     redact_id,
        //                     &pdu.sender,
        //                     &pdu.room_id,
        //                     false,
        //                 )? {
        //                     return Err(Error::BadRequest(
        //                         ErrorKind::forbidden(),
        //                         "User cannot redact this event.",
        //                     ));
        //                 }
        //             };
        //         }
        //         _ => {
        //             let content =
        //                 serde_json::from_str::<RoomRedactionEventContent>(pdu.content.get())
        //                     .map_err(|_| {
        //                         Error::bad_database("Invalid content in redaction pdu.")
        //                     })?;

        //             if let Some(redact_id) = &content.redacts {
        //                 if !self.services.state_accessor.user_can_redact(
        //                     redact_id,
        //                     &pdu.sender,
        //                     &pdu.room_id,
        //                     false,
        //                 )? {
        //                     return Err(Error::BadRequest(
        //                         ErrorKind::forbidden(),
        //                         "User cannot redact this event.",
        //                     ));
        //                 }
        //             }
        //         }
        //     }
        // };

        // We append to state before appending the pdu, so we don't have a moment in
        // time with the pdu without it's state. This is okay because append_pdu can't
        // fail.
        // let statehashid = self.services.state.append_to_state(&pdu)?;

        let pdu_id = self
            .append_pdu(
                &pdu,
                pdu_json,
                // Since this PDU references all pdu_leaves we can update the leaves
                // of the room
                vec![(*pdu.event_id).to_owned()],
                state_lock,
            )
            .await?;

        // We set the room state after inserting the pdu, so that we never have a moment
        // in time where events in the current room state do not exist
        // self.services
        //     .state
        //     .set_room_state(room_id, statehashid, state_lock)?;

        let mut servers: HashSet<OwnedServerName> = self
            .services
            .state_cache
            .room_servers(room_id)?
            .into_iter()
            .collect();

        // In case we are kicking or banning a user, we need to inform their server of
        // the change
        if pdu.kind == TimelineEventType::RoomMember {
            if let Some(state_key_uid) = &pdu
                .state_key
                .as_ref()
                .and_then(|state_key| UserId::parse(state_key.as_str()).ok())
            {
                servers.insert(state_key_uid.server_name().to_owned());
            }
        }

        // Remove our server from the server list since it will be added to it by
        // room_servers() and/or the if statement above
        servers.remove(&self.services.config.server_name);

        self.services
            .sending
            .send_pdu_servers(servers.into_iter(), pdu_id.as_bytes())?;

        Ok(pdu.event_id)
    }

    /// Append the incoming event setting the state snapshot to the state from
    /// the server that sent the event.
    #[tracing::instrument(skip_all)]
    pub async fn append_incoming_pdu(
        &self,
        pdu: &PduEvent,
        pdu_json: CanonicalJsonObject,
        new_room_leaves: Vec<OwnedEventId>,
        // state_ids_compressed: Arc<HashSet<CompressedStateEvent>>,
        soft_fail: bool,
        state_lock: &OwnedMutexGuard<()>, // Take mutex guard to make sure users get the room state mutex
    ) -> Result<Option<Vec<u8>>> {
        // We append to state before appending the pdu, so we don't have a moment in
        // time with the pdu without it's state. This is okay because append_pdu can't
        // fail.
        unimplemented!()
        // self.services
        //     .state
        //     .set_event_state(&pdu.event_id, &pdu.room_id, state_ids_compressed)?;

        // if soft_fail {
        //     self.services
        //         .pdu_metadata
        //         .mark_as_referenced(&pdu.room_id, &pdu.prev_events)?;
        //     self.services.state.set_forward_extremities(
        //         &pdu.room_id,
        //         new_room_leaves,
        //         state_lock,
        //     )?;
        //     return Ok(None);
        // }

        // let pdu_id = self
        //     .append_pdu(pdu, pdu_json, new_room_leaves, state_lock)
        //     .await?;

        // Ok(Some(pdu_id))
    }
}
